/**
 ******************************************************************************
 * @addtogroup OpenPilotModules OpenPilot Modules
 * @{
 * @addtogroup MKSerialModule MK Serial Control Module
 * @brief Connect to MK module
 * @{
 *
 * @file       MKSerial.c
 * @author     The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
 * @brief      Interfacing with MK via serial port
 * @see        The GNU Public License (GPL) Version 3
 *
 *****************************************************************************/
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "openpilot.h"
#include "MkSerial.h"

#include "attitudestate.h" // object that will be updated by the module
#include "positionstate.h"
#include "flightbatterystate.h"

//
// Configuration
//
#define PORT          PIOS_COM_AUX
#define DEBUG_PORT    PIOS_COM_GPS
#define STACK_SIZE    1024
#define TASK_PRIORITY (tskIDLE_PRIORITY + 3)
#define MAX_NB_PARS   100
// #define ENABLE_DEBUG_MSG
// #define GENERATE_BATTERY_INFO         // The MK can report battery voltage, but normally the current sensor will be used, so this module should not report battery state

#if PORT == PIOS_COM_AUX
#ifndef PIOS_ENABLE_AUX_UART
#error "This module cannot be included without the AUX UART enabled"
#endif
#endif
//
// Private constants
//
#define MSGCMD_ANY              0
#define MSGCMD_GET_DEBUG        'd'
#define MSGCMD_DEBUG            'D'
#define MSGCMD_GET_VERSION      'v'
#define MSGCMD_VERSION          'V'
#define MSGCMD_GET_OSD          'o'
#define MSGCMD_OSD              'O'

#define DEBUG_MSG_NICK_IDX      (2 + 2 * 2)
#define DEBUG_MSG_ROLL_IDX      (2 + 3 * 2)

#define OSD_MSG_CURRPOS_IDX     1
#define OSD_MSG_NB_SATS_IDX     50
#define OSD_MSG_BATT_IDX        57
#define OSD_MSG_GNDSPEED_IDX    58
#define OSD_MSG_COMPHEADING_IDX 62
#define OSD_MSG_NICK_IDX        64
#define OSD_MSG_ROLL_IDX        65

#ifdef ENABLE_DEBUG_MSG
#define DEBUG_MSG(format, ...) PIOS_COM_SendFormattedString(DEBUG_PORT, format,##__VA_ARGS__)
#else
#define DEBUG_MSG(format, ...)
#endif

//
// Private types
//
typedef struct {
    uint8_t address;
    uint8_t cmd;
    uint8_t nbPars;
    uint8_t pars[MAX_NB_PARS];
} MkMsg_t;

typedef struct {
    float   longitute;
    float   latitude;
    float   altitude;
    uint8_t status;
} GpsPosition_t;

enum {
    MK_ADDR_ALL = 0,
    MK_ADDR_FC  = 1,
    MK_ADDR_NC  = 2,
    MK_ADDR_MAG = 3,
};

//
// Private variables
//

//
// Private functions
//
static void OnError(int line);
// static void PrintMsg(const MkMsg_t* msg);
static int16_t Par2Int16(const MkMsg_t *msg, uint8_t index);
static int32_t Par2Int32(const MkMsg_t *msg, uint8_t index);
static int8_t Par2Int8(const MkMsg_t *msg, uint8_t index);
static void GetGpsPos(const MkMsg_t *msg, uint8_t index, GpsPosition_t *pos);
static uint8_t WaitForBytes(uint8_t *buf, uint8_t nbBytes, portTickType xTicksToWait);
static bool WaitForMsg(uint8_t cmd, MkMsg_t *msg, portTickType xTicksToWait);
static void SendMsg(const MkMsg_t *msg);
static void SendMsgParNone(uint8_t address, uint8_t cmd);
static void SendMsgPar8(uint8_t address, uint8_t cmd, uint8_t par0);
static void MkSerialTask(void *parameters);

static void OnError(int line)
{
    DEBUG_MSG("MKProcol error %d\n\r", line);
}

#if 0
static void PrintMsg(const MkMsg_t *msg)
{
    switch (msg->address) {
    case MK_ADDR_ALL:
        DEBUG_MSG("ALL ");
        break;
    case MK_ADDR_FC:
        DEBUG_MSG("FC  ");
        break;
    case MK_ADDR_NC:
        DEBUG_MSG("NC  ");
        break;
    case MK_ADDR_MAG:
        DEBUG_MSG("MAG ");
        break;
    default:
        DEBUG_MSG("??? ");
        break;
    }

    DEBUG_MSG("%c ", msg->cmd);

    for (int i = 0; i < msg->nbPars; i++) {
        DEBUG_MSG("%02x ", msg->pars[i]);
    }
    DEBUG_MSG("\n\r");
}
#endif /* if 0 */

static int16_t Par2Int16(const MkMsg_t *msg, uint8_t index)
{
    int16_t res;

    res = (int)(msg->pars[index + 1]) * 256 + msg->pars[index];
    if (res > 0xFFFF / 2) {
        res -= 0xFFFF;
    }
    return res;
}

static int32_t Par2Int32(const MkMsg_t *msg, uint8_t index)
{
    uint32_t val = 0;

    val  = (((int)msg->pars[index]) << 0) + (((int)msg->pars[index + 1]) << 8);
    val += (((int)msg->pars[index + 2]) << 16) + ((int)msg->pars[index + 3] << 24);
    if (val > 0xFFFFFFFF / 2) {
        val -= 0xFFFFFFFF;
    }
    return (int32_t)val;
}

static int8_t Par2Int8(const MkMsg_t *msg, uint8_t index)
{
    if (msg->pars[index] > 127) {
        return msg->pars[index] - 256;
    } else {
        return msg->pars[index];
    }
}

static void GetGpsPos(const MkMsg_t *msg, uint8_t index, GpsPosition_t *pos)
{
    pos->longitute = (float)Par2Int32(msg, index) * (float)1e-7;
    pos->latitude  = (float)Par2Int32(msg, index + 4) * (float)1e-7;
    pos->altitude  = (float)Par2Int32(msg, index + 8) * (float)1e-3;
    pos->status    = msg->pars[index + 12];
}

static uint8_t WaitForBytes(uint8_t *buf, uint8_t nbBytes, portTickType xTicksToWait)
{
    uint8_t nbBytesLeft = nbBytes;
    xTimeOutType xTimeOut;

    vTaskSetTimeOutState(&xTimeOut);

    // Loop until
    // - all bytes are received
    // - \r is seen
    // - Timeout occurs
    do {
        // Check if timeout occured
        if (xTaskCheckForTimeOut(&xTimeOut, &xTicksToWait)) {
            break;
        }

        // Check if there are some bytes
        if (PIOS_COM_ReceiveBufferUsed(PORT)) {
            *buf = PIOS_COM_ReceiveBuffer(PORT);

            nbBytesLeft--;
            if (buf[0] == '\r') {
                break;
            }
            buf++;
        } else {
            // Avoid tight loop
            // FIXME: should be blocking
            vTaskDelay(5);
        }
    } while (nbBytesLeft);

    return nbBytes - nbBytesLeft;
}

static bool WaitForMsg(uint8_t cmd, MkMsg_t *msg, portTickType xTicksToWait)
{
    uint8_t buf[10];
    uint8_t n;
    bool done  = FALSE;
    bool error = FALSE;
    unsigned int checkVal;
    xTimeOutType xTimeOut;

    vTaskSetTimeOutState(&xTimeOut);

    while (!done && !error) {
        // When we are here, it means we did not encounter the message we are waiting for
        // Check if we did not timeout yet.

        // Wait for start
        buf[0] = 0;
        do {
            if (xTaskCheckForTimeOut(&xTimeOut, &xTicksToWait)) {
                return FALSE;
            }
            WaitForBytes(buf, 1, 100 / portTICK_RATE_MS);
        } while (buf[0] != '#');

        // Wait for cmd and address
        if (WaitForBytes(buf, 2, 10 / portTICK_RATE_MS) != 2) {
            OnError(__LINE__);
            continue;
        }
        // Is this the command we are waiting for?
        if (cmd == 0 || cmd == buf[1]) {
            // OK follow this message to the end
            msg->address = buf[0] - 'a';
            msg->cmd     = buf[1];

            checkVal     = '#' + buf[0] + buf[1];

            // Parse parameters
            msg->nbPars  = 0;
            while (!done && !error) {
                n = WaitForBytes(buf, 4, 10 / portTICK_RATE_MS);
                if (n > 0 && buf[n - 1] == '\r') {
                    n--;
                    // This is the end of the message
                    // Get check bytes
                    if (n >= 2) {
                        unsigned int msgCeckVal;
                        msgCeckVal = (buf[n - 1] - '=') + (buf[n - 2] - '=') * 64;
                        // printf("%x %x\n", msgCeckVal, checkVal&0xFFF);
                        n -= 2;

                        if (msgCeckVal == (checkVal & 0xFFF)) {
                            done = TRUE;
                        } else {
                            OnError(__LINE__);
                            error = TRUE;
                        }
                    } else {
                        OnError(__LINE__);
                        error = TRUE;
                    }
                } else if (n == 4) {
                    // Parse parameters
                    int i;
                    for (i = 0; i < 4; i++) {
                        checkVal += buf[i];
                        buf[i]   -= '=';
                    }
                    if (msg->nbPars < MAX_NB_PARS) {
                        msg->pars[msg->nbPars] = (((buf[0] << 2) & 0xFF) | ((buf[1] >> 4)));
                        msg->nbPars++;
                    }
                    if (msg->nbPars < MAX_NB_PARS) {
                        msg->pars[msg->nbPars] = ((buf[1] & 0x0F) << 4 | (buf[2] >> 2));
                        msg->nbPars++;
                    }
                    if (msg->nbPars < MAX_NB_PARS) {
                        msg->pars[msg->nbPars] = ((buf[2] & 0x03) << 6 | buf[3]);
                        msg->nbPars++;
                    }
                } else {
                    OnError(__LINE__);
                    error = TRUE;
                }
            }
        }
    }

    return done && !error;
}

static void SendMsg(const MkMsg_t *msg)
{
    uint8_t buf[10];
    uint16_t checkVal;
    uint8_t nbParsRemaining;
    const uint8_t *pPar;

    // Header
    buf[0]   = '#';
    buf[1]   = msg->address + 'a';
    buf[2]   = msg->cmd;
    PIOS_COM_SendBuffer(PORT, buf, 3);
    checkVal = (unsigned int)'#' + buf[1] + buf[2];

    // Parameters
    nbParsRemaining = msg->nbPars;
    pPar = msg->pars;
    while (nbParsRemaining) {
        uint8_t a, b, c;

        a = *pPar;
        b = 0;
        c = 0;

        nbParsRemaining--;
        pPar++;
        if (nbParsRemaining) {
            b = *pPar;
            nbParsRemaining--;
            pPar++;
            if (nbParsRemaining) {
                c = *pPar;
                nbParsRemaining--;
                pPar++;
            }
        }

        buf[0]    = (a >> 2) + '=';
        buf[1]    = (((a & 0x03) << 4) | ((b & 0xf0) >> 4)) + '=';
        buf[2]    = (((b & 0x0f) << 2) | ((c & 0xc0) >> 6)) + '=';
        buf[3]    = (c & 0x3f) + '=';
        checkVal += buf[0];
        checkVal += buf[1];
        checkVal += buf[2];
        checkVal += buf[3];

        PIOS_COM_SendBuffer(PORT, buf, 4);
    }

    checkVal &= 0xFFF;
    buf[0]    = (checkVal / 64) + '=';
    buf[1]    = (checkVal % 64) + '=';
    buf[2]    = '\r';
    PIOS_COM_SendBuffer(PORT, buf, 3);
}

static void SendMsgParNone(uint8_t address, uint8_t cmd)
{
    MkMsg_t msg;

    msg.address = address;
    msg.cmd     = cmd;
    msg.nbPars  = 0;

    SendMsg(&msg);
}

static void SendMsgPar8(uint8_t address, uint8_t cmd, uint8_t par0)
{
    MkMsg_t msg;

    msg.address = address;
    msg.cmd     = cmd;
    msg.nbPars  = 1;
    msg.pars[0] = par0;

    SendMsg(&msg);
}

static uint16_t VersionMsg_GetVersion(const MkMsg_t *msg)
{
    return msg->pars[0] * 100 + msg->pars[1];
}

static void DoConnectedToFC(void)
{
    AttitudeStateData attitudeData;
    MkMsg_t msg;

    DEBUG_MSG("FC\n\r");

    memset(&attitudeData, 0, sizeof(attitudeData));

    // Configure FC for fast reporting of the debug-message
    SendMsgPar8(MK_ADDR_ALL, MSGCMD_GET_DEBUG, 10);

    while (TRUE) {
        if (WaitForMsg(MSGCMD_DEBUG, &msg, 500 / portTICK_RATE_MS)) {
            int16_t nick;
            int16_t roll;

            // PrintMsg(&msg);
            nick = Par2Int16(&msg, DEBUG_MSG_NICK_IDX);
            roll = Par2Int16(&msg, DEBUG_MSG_ROLL_IDX);

            DEBUG_MSG("Att: Nick=%5d Roll=%5d\n\r", nick, roll);

            attitudeData.Pitch = -(float)nick / 10;
            attitudeData.Roll  = -(float)roll / 10;
            AttitudeStateSet(&attitudeData);
        } else {
            DEBUG_MSG("TO\n\r");
            break;
        }
    }
}

static void DoConnectedToNC(void)
{
    MkMsg_t msg;
    GpsPosition_t pos;
    AttitudeStateData attitudeData;
    PositionStateData positionData;
    FlightBatteryStateData flightBatteryData;

#ifdef GENERATE_BATTERY_INFO
    uint8_t battStateCnt = 0;
#endif

    DEBUG_MSG("NC\n\r");

    memset(&attitudeData, 0, sizeof(attitudeData));
    memset(&positionData, 0, sizeof(positionData));
    memset(&flightBatteryData, 0, sizeof(flightBatteryData));

    // Configure NC for fast reporting of the osd-message
    SendMsgPar8(MK_ADDR_ALL, MSGCMD_GET_OSD, 10);

    while (TRUE) {
        if (WaitForMsg(MSGCMD_OSD, &msg, 500 / portTICK_RATE_MS)) {
            // PrintMsg(&msg);
            GetGpsPos(&msg, OSD_MSG_CURRPOS_IDX, &pos);

#if 0
            DEBUG_MSG("Bat=%d\n\r", msg.pars[OSD_MSG_BATT_IDX]);
            DEBUG_MSG("Nick=%d Roll=%d\n\r", Par2Int8(&msg, OSD_MSG_NICK_IDX), Par2Int8(&msg, OSD_MSG_ROLL_IDX));
            DEBUG_MSG("POS #Sats=%d stat=%d lat=%d lon=%d alt=%d\n\r", msg.pars[OSD_MSG_NB_SATS_IDX], pos.status, (int)pos.latitude,
                      (int)pos.longitute, (int)pos.altitude);
#else
            DEBUG_MSG(".");
#endif
            attitudeData.Pitch       = -Par2Int8(&msg, OSD_MSG_NICK_IDX);
            attitudeData.Roll        = -Par2Int8(&msg, OSD_MSG_ROLL_IDX);
            AttitudeStateSet(&attitudeData);

            positionData.Longitude   = pos.longitute;
            positionData.Latitude    = pos.latitude;
            positionData.Altitude    = pos.altitude;
            positionData.Satellites  = msg.pars[OSD_MSG_NB_SATS_IDX];
            positionData.Heading     = Par2Int16(&msg, OSD_MSG_COMPHEADING_IDX);
            positionData.Groundspeed = ((float)Par2Int16(&msg, OSD_MSG_GNDSPEED_IDX)) / 100 /* cm/s => m/s */;
            if (positionData.Satellites < 5) {
                positionData.Status = POSITIONACTUAL_STATUS_NOFIX;
            } else {
                positionData.Status = POSITIONACTUAL_STATUS_FIX3D;
            }
            PositionStateSet(&positionData);

#if GENERATE_BATTERY_INFO
            if (++battStateCnt > 2) {
                flightBatteryData.Voltage = (float)msg.pars[OSD_MSG_BATT_IDX] / 10;
                FlightBatteryStateSet(&flightBatteryData);
                battStateCnt = 0;
            }
#endif
        } else {
            DEBUG_MSG("TO\n\r");
            break;
        }
    }
}

static void MkSerialTask(__attribute__((unused)) void *parameters)
{
    MkMsg_t msg;
    uint32_t version;
    bool connectionOk = FALSE;

    PIOS_COM_ChangeBaud(PORT, 57600);
    PIOS_COM_ChangeBaud(DEBUG_PORT, 57600);

    DEBUG_MSG("MKSerial Started\n\r");

    while (1) {
        // Wait until we get version from MK
        while (!connectionOk) {
            SendMsgParNone(MK_ADDR_ALL, MSGCMD_GET_VERSION);
            DEBUG_MSG("Version... ");
            if (WaitForMsg(MSGCMD_VERSION, &msg, 250 / portTICK_RATE_MS)) {
                version = VersionMsg_GetVersion(&msg);
                DEBUG_MSG("%d\n\r", version);
                connectionOk = TRUE;
            } else {
                DEBUG_MSG("TO\n\r");
            }
        }

        // Dependent on version, decide it we are connected to NC or FC
        // TODO: use slave-addr to distinguish FC/NC -> much safer
        if (version < 60) {
            DoConnectedToNC(); // Will only return after an error
        } else {
            DoConnectedToFC(); // Will only return after an error
        }

        connectionOk = FALSE;

        vTaskDelay(250 / portTICK_RATE_MS);
    }
}

/**
 * Initialise the module
 * \return -1 if initialisation failed
 * \return 0 on success
 */
int32_t MKSerialInitialize(void)
{
    // Start gps task
    xTaskCreate(MkSerialTask, (signed char *)"MkSerial", STACK_SIZE, NULL, TASK_PRIORITY, NULL);

    return 0;
}

/**
 * @}
 * @}
 */
